﻿// <copyright file="Inbox.svc.cs" company="Abadi Kurniawan, Brian Dukes">
// Copyright (c) 2010 Abadi Kurniawan, Brian Dukes
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 
// USE OR OTHER DEALINGS IN THE SOFTWARE. 
// </copyright>

namespace Hackathon.Dnn.MyMessagesInbox
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Security.Authentication;
    using System.ServiceModel;
    using System.ServiceModel.Channels;
    using System.ServiceModel.Web;
    using System.Text;
    using System.Web.Script.Serialization;

    using DotNetNuke.Common.Utilities;
    using DotNetNuke.Entities.Host;
    using DotNetNuke.Entities.Portals;
    using DotNetNuke.Entities.Users;
    using DotNetNuke.Security.Membership;
    using DotNetNuke.Security.Roles;
    using DotNetNuke.Services.Messaging;
    using DotNetNuke.Services.Messaging.Data;

    /// <summary>
    /// A service to provide a user's messages
    /// </summary>
    /// <remarks>
    /// Service setup and <see cref="WriteJson"/> method based on http://www.codethinked.com/post/2010/08/17/WCF-Web-Services-The-Easy-Way.aspx
    /// </remarks>
    [ServiceContract]
    public class Inbox
    {
        /// <summary>
        /// The cache key for the authentication token, with a placeholder for the token
        /// </summary>
        private const string AuthenticationTokenCacheKeyFormat = "MyMessagesInbox Token: {0}";

        /// <summary>
        /// The messaging controller to use to get access to messages
        /// </summary>
        private readonly IMessagingController messagingController;

        /// <summary>
        /// The ID of the portal ID for which this request has been placed.
        /// </summary>
        private readonly int portalId;

        /// <summary>
        /// Initializes a new instance of the <see cref="Inbox"/> class.
        /// </summary>
        public Inbox() : this(new MessagingController(new MessagingDataService()))
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Inbox"/> class.
        /// </summary>
        /// <param name="messagingController">The messaging controller.</param>
        public Inbox(IMessagingController messagingController)
        {
            this.messagingController = messagingController;
            this.portalId = ResolvePortalId();
        }

        /// <summary>
        /// Authenticates the specified user.
        /// </summary>
        /// <remarks>
        /// Based on http://www.gooddogs.com/Blog/tabid/97/EntryId/53/Part-8-DotNetNuke-WCF-Service-Security.aspx
        /// </remarks>
        /// <param name="userName">The username.</param>
        /// <param name="password">The password.</param>
        /// <returns>An authentication token</returns>
        [WebGet]
        [OperationContract]
        public Stream Authenticate(string userName, string password)
        {
            var userId = this.AuthenticateUser(userName, password);

            var token = Guid.NewGuid().ToString();

            UpdateAuthenticationCache(token, new AuthenticationDetails(userId));

            return WriteJson(new { Token = token });
        }

        /// <summary>
        /// Gets the messages for a given user.  Only retrieves the messages that we don't have a record of sending before to this authentiction token
        /// </summary>
        /// <param name="token">The user's authentication token.</param>
        /// <returns>
        /// A stream with a JSON representation of a list of <see cref="DotNetNuke.Services.Messaging.Data.Message"/> instances
        /// </returns>
        [WebGet]
        [OperationContract]
        public Stream GetMessages(string token)
        {
            var authenticationDetails = GetAuthenticationDetails(token);
            var allMessages = this.messagingController.GetUserInbox(this.portalId, authenticationDetails.UserId, 1, int.MaxValue);
            var messages = (from message in allMessages
                           where !authenticationDetails.MessageIds.Contains(message.MessageID)
                           let toRole = Null.IsNull(message.ToUserID) ? new RoleController().GetRole(message.ToRoleID, this.portalId) : null
                           select new
                               {
                                   message.MessageID,
                                   message.Body,
                                   message.Subject,
                                   message.Conversation,
                                   message.FromUserName,
                                   message.ToUserName,
                                   ToRoleName = toRole == null ? string.Empty : toRole.RoleName,
                                   MessageDate = message.MessageDate.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture),
                                   Status = message.Status.ToString(),
                                   message.AllowReply
                               }).ToList();

            authenticationDetails.MessageIds.AddRange(messages.Select(message => message.MessageID));
            UpdateAuthenticationCache(token, authenticationDetails);

            return WriteJson(messages);
        }

        /// <summary>
        /// Gets the user ID associated with the given <paramref name="token"/>.
        /// </summary>
        /// <param name="token">The security token.</param>
        /// <returns>The ID of the user associated with the token</returns>
        private static AuthenticationDetails GetAuthenticationDetails(string token)
        {
            var authenticationDetails =
                DataCache.GetCache<AuthenticationDetails>(string.Format(CultureInfo.InvariantCulture, AuthenticationTokenCacheKeyFormat, token));
            if (authenticationDetails == null)
            {
                throw new AuthenticationException("Security token has expired, please login again");
            }

            return authenticationDetails;
        }

        /// <summary>
        /// Gets the portal ID from which the request was sent.
        /// </summary>
        /// <remarks>
        /// Based on Chris Hammond's example service at http://dnnweb.codeplex.com
        /// </remarks>
        /// <returns>The portal ID of the current request</returns>
        private static int ResolvePortalId()
        {
            if (OperationContext.Current != null)
            {
                var requestUri = OperationContext.Current.IncomingMessageHeaders.To;
                var requestUrl = requestUri.GetLeftPart(UriPartial.Path);
                var urlSchemeLength = requestUri.Scheme.Length + Uri.SchemeDelimiter.Length;
                var portalAlias = requestUrl.Substring(urlSchemeLength, requestUrl.IndexOf("/DesktopModules", StringComparison.OrdinalIgnoreCase) - urlSchemeLength);
                if (!string.IsNullOrEmpty(portalAlias))
                {
                    var aliasInfo = PortalAliasController.GetPortalAliasInfo(portalAlias);
                    if (aliasInfo != null)
                    {
                        return aliasInfo.PortalID;
                    }
                }

                var aliases = PortalAliasController.GetPortalAliasLookup();
                return (from DictionaryEntry aliasPair in aliases
                        let alias = (PortalAliasInfo)aliasPair.Value
                        where requestUrl.StartsWith(alias.HTTPAlias, StringComparison.OrdinalIgnoreCase)
                        orderby alias.HTTPAlias.Length descending // get longest alias, so we prefer child portals over parent portals
                        select alias.PortalID).FirstOrDefault();
            }

            return Host.HostPortalID;
        }

        /// <summary>
        /// Updates the cache of authentication-related info.
        /// </summary>
        /// <param name="token">The token.</param>
        /// <param name="authenticationDetails">The authentication details.</param>
        private static void UpdateAuthenticationCache(string token, AuthenticationDetails authenticationDetails)
        {
            DataCache.SetCache(
                string.Format(CultureInfo.InvariantCulture, AuthenticationTokenCacheKeyFormat, token),
                authenticationDetails,
                new TimeSpan(0, 5, 0));
        }

        /// <summary>
        /// Writes the value as JSON.
        /// </summary>
        /// <param name="value">The value to write as JSON.</param>
        /// <returns>A stream whose content is the <paramref name="value"/> as JSON</returns>
        private static Stream WriteJson(object value)
        {
            var javaScriptSerializer = new JavaScriptSerializer();
            var json = Encoding.UTF8.GetBytes(javaScriptSerializer.Serialize(value));

            MemoryStream memoryStream = null;
            try
            {
                memoryStream = new MemoryStream(json);

                if (WebOperationContext.Current != null)
                {
                    WebOperationContext.Current.OutgoingResponse.ContentType = "application/json; charset=utf-8";
                }

                return memoryStream;
            }
            catch
            {
                // make sure to dispose of the stream if we don't get it returned
                if (memoryStream != null)
                {
                    memoryStream.Dispose();
                }

                throw;
            }
        }

        /// <summary>
        /// Get the ID of the user, based on userName and password.  Throws an <see cref="AuthenticationException"/> if login fails
        /// </summary>
        /// <param name="userName">The username.</param>
        /// <param name="password">The password.</param>
        /// <returns>The user ID</returns>
        private int AuthenticateUser(string userName, string password)
        {
            var loginStatus = UserLoginStatus.LOGIN_FAILURE;
            string ipAddress;
            if (OperationContext.Current == null)
            {
                ipAddress = string.Empty;
            }
            else
            {
                var endpointProperty = (RemoteEndpointMessageProperty)OperationContext.Current.IncomingMessageProperties[RemoteEndpointMessageProperty.Name];
                ipAddress = endpointProperty.Address;
            }

            var userInfo = UserController.ValidateUser(this.portalId, userName, password, string.Empty, string.Empty, ipAddress, ref loginStatus);
            if (loginStatus != UserLoginStatus.LOGIN_SUCCESS && loginStatus != UserLoginStatus.LOGIN_SUPERUSER)
            {
                throw new AuthenticationException("Failure to authenticate.  Error was " + loginStatus.ToString());
            }

            return userInfo.UserID;
        }

        /// <summary>
        /// Contains information tied to an authentication token
        /// </summary>
        private class AuthenticationDetails
        {
            /// <summary>
            /// Initializes a new instance of the <see cref="Hackathon.Dnn.MyMessagesInbox.Inbox.AuthenticationDetails"/> class.
            /// </summary>
            /// <param name="userId">The user ID.</param>
            public AuthenticationDetails(int userId)
            {
                this.UserId = userId;
                this.MessageIds = new List<int>();
            }

            /// <summary>
            /// Gets the user ID.
            /// </summary>
            /// <value>The user ID.</value>
            public int UserId { get; private set; }

            /// <summary>
            /// Gets the list of the IDs of the messages that have been sent to the phone already.
            /// </summary>
            /// <value>The message ID list.</value>
            public List<int> MessageIds { get; private set; }
        }
    }
}
